﻿using System;
using System.Collections.Generic;
using System.Disposables;
using System.Linq;
using Collab.Services.Remote.Entities;
using System.Diagnostics;

namespace Collab.Modules.Editing
{
    public class ChangeProcessor : IChangeProcessor
    {
        private readonly CompositeDisposable disposable = new CompositeDisposable();
        private IList<BufferUpdate> updates = new List<BufferUpdate>();
        private Subject<BufferUpdate> input = new Subject<BufferUpdate>();
        private Subject<BufferUpdate> output = new Subject<BufferUpdate>();

        public int Revision { get; private set; }

        public ChangeProcessor()
        {
            disposable.Add(this.input.ObserveOnDispatcher().Subscribe(ProcessUpdate));
        }

        private void ProcessUpdate(BufferUpdate item)
        {
            if (item == null)
                throw new ArgumentNullException("item");

            BufferUpdate newUpdate = SynchronizeUpdate(item);

            this.updates.Add(newUpdate);
            this.output.OnNext(newUpdate);

        }

        private BufferUpdate SynchronizeUpdate(BufferUpdate item)
        {
            DateTime startTime = DateTime.Now;
            //Debug.WriteLine("Update:" + item.Revision);
            if (item == null)
                throw new ArgumentNullException("item");

            if (this.updates.Count == 0)
            {
#if DEBUG

                //UnitTestBuilder.Updates.Add(new Tuple<BufferUpdate, BufferUpdate, IEnumerable<BufferUpdate>>(item, item, null));
                UnitTestBuilder.AddUpdateInformation(startTime, item, DateTime.Now, item, null);
#endif
                return item;
            }
            int lastRev = this.updates.Last().Revision;
            //If the new update has a revision that is <= to the last update in the buffer we need to recalculate
            if (item.Revision <= lastRev)
            {
                int changesToSkip = this.updates.Count - (lastRev + 1 - item.Revision);
                //Find the updates made by the user that has already been syncronized and now have a higher rev then the incomming change
                int multiSyncCount = this.updates.Skip(changesToSkip)
                    .Where(c => c.Sender == item.Sender)
                    .Count();
                int temp = multiSyncCount;
                while (temp > 0)
                {
                    multiSyncCount += temp = this.updates.Skip(changesToSkip - multiSyncCount).Take(temp)
                    .Where(c => c.Sender == item.Sender)
                    .Count();
                }



                var surpassedUpdates = this.updates.Skip(changesToSkip - multiSyncCount).Where(update => item.Sender != update.Sender);
                var ownChanges = this.updates.Skip(changesToSkip - multiSyncCount).Where(update => item.Sender == update.Sender).SelectMany(u => u.Changes);

                var changes = item.Changes.Select(change =>
                {
                    int newStart = surpassedUpdates
                        .SelectMany(u => u.Changes)
                        .Aggregate(change.OldStart, (start, savedChange) => {
                            int ownDelta = ownChanges.Where(c => c.OldStart <= savedChange.OldStart).Sum(c => c.Delta);
                            return savedChange.OldStart + ownDelta <= start ? start + savedChange.Delta : start;
                        });
                    return new Change(newStart, newStart + (change.OldEnd - change.OldStart), change.Delta, change.Text);
                });

                int newRev = lastRev + 1;
                var newUpdate = new BufferUpdate(item.DocumentName, item.Sender, newRev, changes.ToList());
#if DEBUG
                UnitTestBuilder.AddUpdateInformation(startTime, item, DateTime.Now, newUpdate, this.updates.Skip(changesToSkip - multiSyncCount).ToArray());
                //UnitTestBuilder.Updates.Add(new Tuple<BufferUpdate, BufferUpdate, IEnumerable<BufferUpdate>>(item, newUpdate, this.updates.Skip(changesToSkip - multiSyncCount).ToArray()));
#endif
                return newUpdate;
            }
            else
            {
                //Make a check to make sure that we are not reciving an update which is not next in turn
                Debug.Assert(this.updates.Last().Revision + 1 - item.Revision == 0);

#if DEBUG
                UnitTestBuilder.AddUpdateInformation(startTime, item, DateTime.Now, item, null);
                //UnitTestBuilder.Updates.Add(new Tuple<BufferUpdate, BufferUpdate, IEnumerable<BufferUpdate>>(item, item, null));
#endif
                return item;
            }

        }

        #region IObserver<BufferUpdate> Members

        public void OnCompleted()
        {
            this.input.OnCompleted();
        }

        public void OnError(Exception error)
        {
            this.input.OnError(error);
        }

        public void OnNext(BufferUpdate value)
        {
            this.input.OnNext(value);
        }

        #endregion

        #region IObservable<BufferUpdate> Members

        public IDisposable Subscribe(IObserver<BufferUpdate> observer)
        {
            return this.output.Subscribe(observer);
        }

        #endregion

        #region IDisposable Members

        public void Dispose()
        {
            this.disposable.Dispose();
        }

        #endregion
    }
}
